package weka.classifiers.meta;

import java.io.BufferedOutputStream;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.Enumeration;
import java.util.Random;
import java.util.Vector;

import weka.classifiers.AbstractClassifier;
import weka.classifiers.Classifier;
import weka.core.Attribute;
import weka.core.Capabilities;
import weka.core.DenseInstance;
import weka.core.Instance;
import weka.core.Instances;
import weka.core.Option;
import weka.core.OptionHandler;
import weka.core.SelectedTag;
import weka.core.Tag;
import weka.core.Utils;
import weka.core.Capabilities.Capability;
import weka.core.converters.ArffSaver;

public class ModelCombiner extends Vote implements OptionHandler {

	/**
	 * 
	 */
	private static final long serialVersionUID = 2502545471553785492L;
	private Classifier combinerClassifier;
	private Classifier[] selecterClassifiers;
	private Instances distdata;
	private boolean m_ProbabilityInstances;
	private double m_Fraction;
	private static int counter = 0;
	/** combination rule: combiner */
	public static final int COMBINER_DIST_RULE = 8;
	/** combination rule: combiner */
	public static final int COMBINER_RULE = 7;
	/** combination rule: combiner selected average */
	public static final int COMBINER_SELECTED_RULE = 9;
	private double[][][] distributions = null;

	public static final Tag[] TAGS_RULES = {
			new Tag(AVERAGE_RULE, "AVG", "Average of Probabilities"),
			new Tag(PRODUCT_RULE, "PROD", "Product of Probabilities"),
			new Tag(MAJORITY_VOTING_RULE, "MAJ", "Majority Voting"),
			new Tag(MIN_RULE, "MIN", "Minimum Probability"),
			new Tag(MAX_RULE, "MAX", "Maximum Probability"),
			new Tag(MEDIAN_RULE, "MED", "Median"),
			new Tag(COMBINER_RULE, "COMB", "Combiner"),
			new Tag(COMBINER_DIST_RULE, "CD", "Distribution based Combiner"),
			new Tag(COMBINER_SELECTED_RULE, "CS", "Combiner Selected Average") };

	/**
	 * String describing default classifier.
	 */
	protected String defaultClassifierString() {

		return "weka.classifiers.functions.MultilayerPerceptron";
	}

	/**
	 * String describing default classifier.
	 */
	protected Classifier defaultClassifier() {

		return new weka.classifiers.functions.MultilayerPerceptron();
	}

	/**
	 * Returns default capabilities of the classifier.
	 * 
	 * @return the capabilities of this classifier
	 */
	public Capabilities getCapabilities() {
		Capabilities result = super.getCapabilities();

		// class
		if ((m_CombinationRule == PRODUCT_RULE)
				|| (m_CombinationRule == MAJORITY_VOTING_RULE)) {
			result.disableAllClasses();
			result.disableAllClassDependencies();
			result.enable(Capability.NOMINAL_CLASS);
			result.enableDependency(Capability.NOMINAL_CLASS);
		} else if (m_CombinationRule == MEDIAN_RULE) {
			result.disableAllClasses();
			result.disableAllClassDependencies();
			result.enable(Capability.NUMERIC_CLASS);
			result.enableDependency(Capability.NUMERIC_CLASS);
		} else if (m_CombinationRule == COMBINER_RULE) {
			result.disableAllClasses();
			result.disableAllClassDependencies();
			if (getProbabilityInstances()) {
				result.enable(Capability.BINARY_CLASS);
				result.enableDependency(Capability.BINARY_CLASS);
			} else {
				result = combinerClassifier.getCapabilities();
			}
		}

		return result;
	}

	/**
	 * Buildclassifier selects a classifier from the set of classifiers by
	 * minimising error on the training data.
	 * 
	 * @param data
	 *            the training data to be used for generating the boosted
	 *            classifier.
	 * @throws Exception
	 *             if the classifier could not be built successfully
	 */
	public void buildClassifier(Instances data) throws Exception {

		// can classifier handle the data?
		getCapabilities().testWithFail(data);

		// remove instances with missing class
		Instances newData = new Instances(data);
		newData.deleteWithMissingClass();

		m_Random = new Random(getSeed());

		ArrayList<Attribute> attr = new ArrayList<Attribute>();
		for (int i = 0; i < m_Classifiers.length; i++)
			attr.add(new Attribute("Var" + i));
		attr.add(data.classAttribute().copy("class"));
		distdata = new Instances("CombinerInstances", attr, data.numInstances());

		if (m_CombinationRule == COMBINER_SELECTED_RULE) {
			selecterClassifiers = new Classifier[m_Classifiers.length];
		} else if (m_CombinationRule == COMBINER_DIST_RULE) {
			selecterClassifiers = new Classifier[data.numClasses()];
		}
		if (isCombiner()) {
			distributions = new double[newData.numInstances()][m_Classifiers.length][];
		}
		for (int i = 0; i < m_Classifiers.length; i++) {
			getClassifier(i).buildClassifier(newData);

			if (isCombiner()) {
				for (int j = 0; j < newData.numInstances(); j++) {
					distributions[j][i] = getClassifier(i).distributionForInstance(newData.instance(j));
				}
			}
			if (m_CombinationRule == COMBINER_SELECTED_RULE) {
				selecterClassifiers[i] = (Classifier) Class.forName(
						getCombinerClassifier().getClass().getName())
						.newInstance();
				((OptionHandler) selecterClassifiers[i])
						.setOptions(((OptionHandler) getCombinerClassifier())
								.getOptions());
				Instances tmpData = new Instances(newData);
				for (int j = 0; j < tmpData.numInstances(); j++) {
					//if (includeInstance(tmpData.instance(j), getClassifier(i))) {
						double prediction = Utils.maxIndex(distributions[j][i]);
						double check = getClassifier(i).classifyInstance(tmpData.instance(j));
						if (prediction == tmpData.instance(j).classValue())
							tmpData.instance(j).setClassValue(1);
						else
							tmpData.instance(j).setClassValue(0);
					//}
				}
				selecterClassifiers[i].buildClassifier(tmpData);
			}
		}

		if (m_CombinationRule == COMBINER_DIST_RULE) {
			for (int c = 0; c < data.numClasses(); c++) {
				distdata.delete();
				for (int j = 0; j < data.numInstances(); j++) {
					if (includeInstance(distributions[j]))
						if (data.instance(j).classValue() == c)
							distdata.add(getCombinerDistInstance(distributions[j], c, 1));
						else
							distdata.add(getCombinerDistInstance(distributions[j], c, 0));
				}
				distdata.setClassIndex(m_Classifiers.length);
				selecterClassifiers[c] = (Classifier) Class.forName(
						getCombinerClassifier().getClass().getName())
						.newInstance();
				selecterClassifiers[c].buildClassifier(distdata);
				if (m_Debug) {
					ArffSaver as = new ArffSaver();
					as.setInstances(distdata);
					String relationName = newData.relationName() + "_"
							+ distdata.relationName();
					as.setDestination(new BufferedOutputStream(
							new FileOutputStream(new File(relationName + "_"
									+ this.getClass().getSimpleName() + " "
									+ counter++ + "_" + c + ".arff"))));
					as.writeBatch();
				}
			}
		}
		if (m_CombinationRule == COMBINER_RULE) {
			for (int j = 0; j < data.numInstances(); j++) {
				if (includeInstance(distributions[j]))
					distdata.add(getCombinerInstance(distributions[j], data.instance(j).classValue()));
			}
			distdata.setClassIndex(m_Classifiers.length);
			getCombinerClassifier().buildClassifier(distdata);
			if (m_Debug) {
				ArffSaver as = new ArffSaver();
				as.setInstances(distdata);
				String relationName = newData.relationName() + "_"
						+ distdata.relationName();
				as.setDestination(new BufferedOutputStream(
						new FileOutputStream(new File(relationName + "_"
								+ this.getClass().getSimpleName() + " "
								+ counter++ + ".arff"))));
				as.writeBatch();
			}
		}
		initializeDiversityMeasureImplementation(this, data);
	}

	private boolean isCombiner() {
		return m_CombinationRule == COMBINER_SELECTED_RULE
		|| m_CombinationRule == COMBINER_DIST_RULE
		|| m_CombinationRule == COMBINER_RULE;
	}

	private boolean includeInstance(Instance instance, Classifier classifier) throws Exception {
		if (m_Fraction == 0)
			return true;
		double[] dist = getAverageDistribution(instance, classifier);
		int max = Utils.maxIndex(dist);
		double maxD = dist[max];
		dist[max] = 0;
		double sndMax = dist[Utils.maxIndex(dist)];
		return maxD < sndMax * m_Fraction;
	}

	private boolean includeInstance(double[][] distributions) throws Exception {
		if (m_Fraction == 0)
			return true;
		double[] dist = distributions[0];
		for (int i = 1; i < m_Classifiers.length; i++) {
			double[] tmp = distributions[i];
			for (int j = 0; j < tmp.length; j++) {
				dist[j] += tmp[j];
			}
		}
		for (int j = 0; j < dist.length; j++) {
			dist[j] /= (double) m_Classifiers.length;
		}
		int max = Utils.maxIndex(dist);
		double maxD = dist[max];
		dist[max] = 0;
		double sndMax = dist[Utils.maxIndex(dist)];
		return maxD < sndMax * m_Fraction;
	}

	private double[] getAverageDistribution(Instance instance, Classifier classifier) throws Exception {
		int tag = m_CombinationRule;
		m_CombinationRule = AVERAGE_RULE;
		double[] dist = classifier.distributionForInstance(instance);
		m_CombinationRule = tag;
		return dist;
	}

	private Instance getCombinerDistInstance(Instance instance, int classIndex)
			throws Exception {
		double[] dist = new double[m_Classifiers.length + 1];

		for (int i = 0; i < m_Classifiers.length; i++) {
			double[] tmp = getClassifier(i).distributionForInstance(instance);
			dist[i] = tmp[classIndex];
		}
		dist[m_Classifiers.length] = instance.classValue();
		Instance inst = new DenseInstance(1, dist);
		inst.setDataset(distdata);
		return inst;
	}

	private Instance getCombinerDistInstance(double[][] distributions, int classIndex, double classValue)
			throws Exception {
		double[] dist = new double[m_Classifiers.length + 1];

		for (int i = 0; i < m_Classifiers.length; i++) {
			double[] tmp = distributions[i];
			dist[i] = tmp[classIndex];
		}
		dist[m_Classifiers.length] = classValue;
		Instance inst = new DenseInstance(1, dist);
		inst.setDataset(distdata);
		return inst;
	}

	protected Instance getCombinerInstance(double[][] distributions, double d) throws Exception {
		double[] dist = new double[m_Classifiers.length + 1];

		for (int i = 0; i < m_Classifiers.length; i++) {
			if (getProbabilityInstances()) {
				double[] tmp = distributions[i];
				dist[i] = tmp[0];
			} else {
				double tmp = Utils.maxIndex(distributions[i]);
				dist[i] = tmp;
			}
		}
		dist[m_Classifiers.length] = d;
		Instance inst = new DenseInstance(1, dist);
		inst.setDataset(distdata);
		return inst;
	}

	protected Instance getCombinerInstance(Instance instance) throws Exception {
		double[] dist = new double[m_Classifiers.length + 1];

		for (int i = 0; i < m_Classifiers.length; i++) {
			if (getProbabilityInstances()) {
				double[] tmp = getClassifier(i).distributionForInstance(instance);
				dist[i] = tmp[0];
			} else {
				double tmp = getClassifier(i).classifyInstance(instance);
				dist[i] = tmp;
			}
		}
		dist[m_Classifiers.length] = instance.classValue();
		Instance inst = new DenseInstance(1, dist);
		inst.setDataset(distdata);
		return inst;
	}

	/**
	 * Classifies the given test instance.
	 * 
	 * @param instance
	 *            the instance to be classified
	 * @return the predicted most likely class for the instance or
	 *         Utils.missingValue() if no prediction is made
	 * @throws Exception
	 *             if an error occurred during the prediction
	 */
	public double classifyInstance(Instance instance) throws Exception {
		double result;
		double[] dist;
		int index;

		switch (m_CombinationRule) {
		case AVERAGE_RULE:
		case PRODUCT_RULE:
		case MAJORITY_VOTING_RULE:
		case MIN_RULE:
		case MAX_RULE:
		case COMBINER_DIST_RULE:
		case COMBINER_SELECTED_RULE:
			dist = distributionForInstance(instance);
			if (instance.classAttribute().isNominal()) {
				index = Utils.maxIndex(dist);
				if (dist[index] == 0)
					result = Utils.missingValue();
				else
					result = index;
			} else if (instance.classAttribute().isNumeric()) {
				result = dist[0];
			} else {
				result = Utils.missingValue();
			}
			break;
		case MEDIAN_RULE:
			result = classifyInstanceMedian(instance);
			break;
		case COMBINER_RULE:
			result = getCombinerClassifier().classifyInstance(
					getCombinerInstance(instance));
			break;
		default:
			throw new IllegalStateException("Unknown combination rule '"
					+ m_CombinationRule + "'!");
		}

		return result;
	}

	/**
	 * Classifies a given instance using the selected combination rule.
	 * 
	 * @param instance
	 *            the instance to be classified
	 * @return the distribution
	 * @throws Exception
	 *             if instance could not be classified successfully
	 */
	public double[] distributionForInstance(Instance instance) throws Exception {

		double[] result = new double[instance.numClasses()];

		switch (m_CombinationRule) {
		case AVERAGE_RULE:
			result = distributionForInstanceAverage(instance);
			break;
		case PRODUCT_RULE:
			result = distributionForInstanceProduct(instance);
			break;
		case MAJORITY_VOTING_RULE:
			result = distributionForInstanceMajorityVoting(instance);
			break;
		case MIN_RULE:
			result = distributionForInstanceMin(instance);
			break;
		case MAX_RULE:
			result = distributionForInstanceMax(instance);
			break;
		case MEDIAN_RULE:
			result[0] = classifyInstance(instance);
			break;
		case COMBINER_RULE:
			if (includeInstance(instance, this))
				result = getCombinerClassifier().distributionForInstance(
					getCombinerInstance(instance));
			else
				result = getAverageDistribution(instance, this);
			break;
		case COMBINER_DIST_RULE:
			if (includeInstance(instance, this)) {
				for (int c = 0; c < instance.numClasses(); c++) {
					double[] dist = selecterClassifiers[c]
							.distributionForInstance(getCombinerDistInstance(
									instance, c));
					result[c] = dist[1];
				}
			} else
				result = getAverageDistribution(instance, this);
			break;
		case COMBINER_SELECTED_RULE:
			double[] include = new double[m_Classifiers.length];
			int numIncluded = 0;
			for (int i = 0; i < m_Classifiers.length; i++) {
				include[i] = selecterClassifiers[i].classifyInstance(instance);
				if (include[i] == 1) {
					numIncluded++;
					double[] dist = getClassifier(i).distributionForInstance(
							instance);
					for (int j = 0; j < dist.length; j++) {
						result[j] += dist[j];
					}
				}
			}
			if (numIncluded > 0) {
				for (int j = 0; j < result.length; j++) {
					result[j] /= (double) m_Classifiers.length;
				}
			} else {
				result = getAverageDistribution(instance, this);
			}
			break;
		default:
			throw new IllegalStateException("Unknown combination rule '"
					+ m_CombinationRule + "'!");
		}

		if (!instance.classAttribute().isNumeric() && (Utils.sum(result) > 0))
			Utils.normalize(result);

		return result;
	}

	/**
	 * Returns an enumeration describing the available options
	 * 
	 * @return an enumeration of all the available options
	 */
	public Enumeration<Option> listOptions() {

		Vector<Option> newVector = new Vector<Option>();

		newVector.addElement(new Option(
				"\tFull name of extraction classifier.\n" + "\t(default: "
						+ defaultClassifierString() + ")", "E", 1, "-E"));

		newVector.addElement(new Option("", "", 0,
				"\nOptions specific to classifier "
						+ getCombinerClassifier().getClass().getName() + ":"));
		Enumeration<Option> enu = ((OptionHandler) getCombinerClassifier())
				.listOptions();
		while (enu.hasMoreElements()) {
			newVector.addElement(enu.nextElement());
		}

		newVector.addElement(new Option(ProbabilityInstancesTipText(), "P", 0,
				"-P"));

		newVector.addElement(new Option(FractionTipText(), "F", 1,
				"-F"));

		Enumeration<Option> en = super.listOptions();
		while (en.hasMoreElements()) {
			Option tmp = en.nextElement();
			newVector.addElement(tmp);
		}

		return newVector.elements();
	}

	/**
	 * Parses a given list of options. Valid options are:
	 * <p>
	 * 
	 * -B classifierstring <br>
	 * Classifierstring should contain the full class name of a scheme included
	 * for selection followed by options to the classifier (required, option
	 * should be used once for each classifier).
	 * <p>
	 * 
	 * @param options
	 *            the list of options as an array of strings
	 * @exception Exception
	 *                if an option is not supported
	 */
	public void setOptions(String[] options) throws Exception {
		String classifierString = Utils.getOption('E', options);
		String[] classifierSpec = Utils.splitOptions(classifierString);
		String classifierName;
		if (classifierSpec.length == 0) {
			classifierName = defaultClassifierString();
		} else {
			classifierName = classifierSpec[0];
			classifierSpec[0] = "";
		}
		setCombinerClassifier(AbstractClassifier.forName(classifierName,
				classifierSpec));

		setProbabilityInstances(Utils.getFlag('P', options));
		
		String strFraction = Utils.getOption('F', options);
		if (strFraction.length() != 0)
			setFraction(Integer.parseInt(strFraction));

		super.setOptions(options);
	}

	/**
	 * Gets the current settings of the Classifier.
	 * 
	 * @return an array of strings suitable for passing to setOptions
	 */
	public String[] getOptions() {

		String[] options;
		Vector<String> result = new Vector<String>();
		if (getProbabilityInstances())
			result.add("-P");

		result.add("-F");
		result.add("" + getFraction());

		result.add("-E");
		result.add(getCombinerClassifier().getClass().getName()
				+ " "
				+ Utils.joinOptions(((OptionHandler) getCombinerClassifier())
						.getOptions()));

		options = super.getOptions();
		for (int i = 0; i < options.length; i++) {
			result.add(options[i]);
		}

		String[] array = new String[result.size()];
		for (int i = 0; i < result.size(); i++)
			array[i] = result.elementAt(i).toString();

		return array; // (String[]) result.toArray(new String[result.size()]);
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String ProbabilityInstancesTipText() {
		return "Adds columns with probability distributions from base classifier for each class.";
	}

	/**
	 * Get the value of cro.
	 * 
	 * @return Value of cro.
	 */
	public boolean getProbabilityInstances() {

		return m_ProbabilityInstances;
	}

	/**
	 * Set the value of cro.
	 * 
	 * @param crossoverProbability
	 *            Value to assign to cro.
	 */
	public void setProbabilityInstances(boolean value) {

		m_ProbabilityInstances = value;
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String FractionTipText() {
		return "The value is used to decide whether an instance should be used to train the combiners. If an instance is _fraction_ times more certain on the most certain class than on the second most, it will not be included.";
	}

	/**
	 * Get the value of cro.
	 * 
	 * @return Value of cro.
	 */
	public double getFraction() {

		return m_Fraction;
	}

	/**
	 * Set the value of cro.
	 * 
	 * @param crossoverProbability
	 *            Value to assign to cro.
	 */
	public void setFraction(double value) {

		m_Fraction = value;
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String CombinerClassifierTipText() {
		return "The rule extraction classifier to be used.";
	}

	/**
	 * Sets the rule extraction classifer.
	 * 
	 * @param classifier
	 *            a rule extraction classifier with all options set.
	 */
	public void setCombinerClassifier(Classifier classifier) {

		this.combinerClassifier = classifier;
	}

	/**
	 * Gets the rule extraction classifer.
	 * 
	 * @return the Rule Extraction Classifier
	 */
	public Classifier getCombinerClassifier() {
		if (combinerClassifier == null)
			combinerClassifier = defaultClassifier();

		return combinerClassifier;
	}

	/**
	 * Returns the tip text for this property
	 * 
	 * @return tip text for this property suitable for displaying in the
	 *         explorer/experimenter gui
	 */
	public String combinationRuleTipText() {
		return "The combination rule used.";
	}

	/**
	 * Gets the combination rule used
	 * 
	 * @return the combination rule used
	 */
	public SelectedTag getCombinationRule() {
		return new SelectedTag(m_CombinationRule, TAGS_RULES);
	}

	/**
	 * Sets the combination rule to use. Values other than
	 * 
	 * @param newRule
	 *            the combination rule method to use
	 */
	public void setCombinationRule(SelectedTag newRule) {
		if (newRule.getTags() == TAGS_RULES)
			m_CombinationRule = newRule.getSelectedTag().getID();
	}

	/**
	 * Output a representation of this classifier
	 * 
	 * @return a string representation of the classifier
	 */
	public String toString() {

		if (m_Classifiers == null) {
			return "Vote: No model built yet.";
		}

		String result = "Vote combines";
		result += " the probability distributions of these base learners:\n";
		for (int i = 0; i < m_Classifiers.length; i++) {
			result += '\t' + getClassifierSpec(i) + '\n';
		}
		result += "using the '";

		switch (m_CombinationRule) {
		case AVERAGE_RULE:
			result += "Average of Probabilities";
			break;

		case PRODUCT_RULE:
			result += "Product of Probabilities";
			break;

		case MAJORITY_VOTING_RULE:
			result += "Majority Voting";
			break;

		case MIN_RULE:
			result += "Minimum Probability";
			break;

		case MAX_RULE:
			result += "Maximum Probability";
			break;

		case MEDIAN_RULE:
			result += "Median Probability";
			break;

		case COMBINER_DIST_RULE:
		case COMBINER_RULE:
			result += "Combiners probability distribution";
			break;
			
		case COMBINER_SELECTED_RULE:
			result += "Average probability distribution of combiner selceted classifers";
			break;

		default:
			throw new IllegalStateException("Unknown combination rule '"
					+ m_CombinationRule + "'!");
		}

		result += "' combination rule \n";

		return result;
	}

}
